package top.v5it.japi.plus.support.limit;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.util.StringValueResolver;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;

/**
 * 限流处理
 *
 * @author zhanpu
 */
@Slf4j
@Aspect
public class JapiLimitAspect implements EmbeddedValueResolverAware {

    /**
     * 内置controller
     */
    private static final String[] CONTROLLER_BUILT_IN = {"top.v5it.japi.plus.extra.web.JapiOrmController", "top.v5it.japi.plus.extra.web.JapiQueryController", "top.v5it.japi.plus.extra.web.JapiBatchController"};
    private static final String PREFIX = "{";
    private static final String SUFFIX = "}";
    private static final String PREFIX_DOLLAR = "${";
    private final JapiLimitHandler japiLimitHandler;
    private StringValueResolver resolver;
    private final JapiLimitCnf japiLimitCnf;

    private static final BiFunction<StringValueResolver, String, String> FUNC_RESOLVER = (t, u) ->
            Optional.of(u)
                    .filter(v -> StrUtil.contains(v, PREFIX_DOLLAR) && StrUtil.contains(v, SUFFIX))
                    .map(t::resolveStringValue)
                    .orElse(u);

    public JapiLimitAspect(JapiLimitHandler japiLimitHandler) {
        this.japiLimitHandler = japiLimitHandler;
        this.japiLimitCnf = new JapiLimitCnf();
    }

    @Pointcut("@annotation(top.v5it.japi.plus.support.limit.JapiLimit)")
    public void limit() {
    }

    /**
     * 前置调用，进行流量限制
     *
     * @param joinPoint
     * @throws FrequencyLimitedException {@link FrequencyLimitedException} 超出限流异常
     */
    @Before("limit()")
    public void beforeLimit(JoinPoint joinPoint) {
        final Optional<JapiLimit> optionalJapiLimit = getCurrentLimit(joinPoint);
        if (!optionalJapiLimit.isPresent()) return;
        final Class<?> targetClass = joinPoint.getTarget().getClass();
        final boolean isBuiltin = ArrayUtil.contains(CONTROLLER_BUILT_IN, targetClass.getName());
        final JapiLimit japiLimit = optionalJapiLimit.get();
        final String srcLimitKey = japiLimit.limitKey();
        String targetLimitKey = srcLimitKey;
        if (isBuiltin) {
            // limitKey为请求地址，解析请求地址
            final Signature signature = joinPoint.getSignature();
            final MethodSignature methodSignature = (MethodSignature) signature;
            final Method method = methodSignature.getMethod();
            final int length = method.getParameterCount();
            final Method mostSpecificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
            final Parameter[] parameters = mostSpecificMethod.getParameters();
            final Object[] args = joinPoint.getArgs();
            for (int i = 0; i < length; i++) {
                final Parameter parameter = parameters[i];
                final Object arg = args[i];
                final String name = PREFIX + parameter.getName() + SUFFIX;
                // 替换参数
                if (StrUtil.contains(targetLimitKey, name)) {
                    targetLimitKey = targetLimitKey.replace(name, Objects.toString(arg));
                }
            }
        } else if (StrUtil.isBlank(srcLimitKey)) {
            targetLimitKey = getDefaultLimitKey(joinPoint);
            if (StrUtil.isBlank(targetLimitKey)) return;
        }
        // 首字符为'/'进行删除
        if (StrUtil.equalsCharAt(targetLimitKey, 0, StrUtil.C_SLASH)) {
            targetLimitKey = StrUtil.removePrefix(targetLimitKey, StrUtil.SLASH);
        }
        final LimitCnf limitCnf = japiLimitCnf.get(targetLimitKey, false).orElse(getLimitCnf(japiLimit));
        // limit为空或者少于0不限流
        final String limit = FUNC_RESOLVER.apply(resolver, limitCnf.getLimit());
        boolean match = StrUtil.isBlank(limit) || (StrUtil.isNotBlank(limit) && Integer.valueOf(limit).compareTo(0) <= 0);
        if (match) return;
        // time为空或者少于0不限流
        final String time = FUNC_RESOLVER.apply(resolver, limitCnf.getTime());
        match = StrUtil.isBlank(time) || (StrUtil.isNotBlank(time) && Integer.valueOf(time).compareTo(0) <= 0);
        if (match) return;
        // 替换剩余'/'为':'
        if (StrUtil.contains(targetLimitKey, StrUtil.C_SLASH))
            targetLimitKey = targetLimitKey.replace(StrUtil.C_SLASH, StrUtil.C_COLON);
        // 针对单个接口独自计算，生成独立的key
        if (JapiLimit.Type.SIGNAL.name().equals(limitCnf.getType())) {
            targetLimitKey += StrUtil.C_COLON + getIpAddress().replace(StrUtil.C_DOT, StrUtil.C_COLON);
        }
        final Long result = japiLimitHandler.handler(targetLimitKey, limit, time);
        if (Objects.nonNull(result) && result.compareTo(0L) == 0) {
            log.warn("+++++ip[{}]请求[{}]超出当前流量最大限制+++++", getIpAddress(), targetLimitKey);
            throw new FrequencyLimitedException();
        }
    }

    private LimitCnf getLimitCnf(JapiLimit japiLimit) {
        final String limitKey = japiLimit.limitKey();
        return japiLimitCnf.get(limitKey)
                .orElseGet(() ->
                        new LimitCnf()
                                .setType(japiLimit.type().name())
                                .setLimit(japiLimit.limit())
                                .setTime(japiLimit.time())
                                .setKey(limitKey)
                );
    }

    /**
     * 获取ip地址
     *
     * @return ip地址
     */
    private String getIpAddress() {
        HttpServletRequest request =
                ((ServletRequestAttributes) Objects
                        .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        return ServletUtil.getClientIP(request);
    }

    /**
     * 请求方法
     *
     * @return
     */
    private String getRequestMethod() {
        HttpServletRequest request =
                ((ServletRequestAttributes) Objects
                        .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        return request.getMethod();
    }

    /**
     * 请求uri
     *
     * @return
     */
    private String getRequestURI() {
        HttpServletRequest request =
                ((ServletRequestAttributes) Objects
                        .requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        return request.getRequestURI();
    }

    /**
     * 获取默认值
     *
     * @param joinPoint
     * @return
     */
    private String getDefaultLimitKey(JoinPoint joinPoint) {
        final Function<RequestMapping, String> requestMappingFunction = t -> {
            if (ArrayUtil.isNotEmpty(t.value())) return t.value()[0];
            if (ArrayUtil.isNotEmpty(t.path())) return t.path()[0];
            return null;
        };
        // controller类上请求路径
        final Optional<String> classOfRequestPath = getClassRequestMapping(joinPoint).map(requestMappingFunction);
        // controller方法上请求路径（RequestMapping）
        final Optional<String> methodOfRequestPath = getMethodRequestMapping(joinPoint, RequestMapping.class).map(requestMappingFunction);
        if (classOfRequestPath.isPresent() && methodOfRequestPath.isPresent()) {
            // 类上和方法上都有，拼装一个整体请求路径
            return getFullPath(classOfRequestPath, methodOfRequestPath);
        } else if (!classOfRequestPath.isPresent() && methodOfRequestPath.isPresent()) {
            // 类上没有，按方法上返回
            return methodOfRequestPath.get();
        } else {
            // controller方法上请求路径（GetMapping|PostMapping|PutMapping|DeleteMapping）
            final String method = getRequestMethod();
            Optional<String> requestPath;
            switch (RequestMethod.valueOf(method)) {
                case GET:
                    requestPath = getMethodRequestMapping(joinPoint, GetMapping.class).map(t -> {
                        if (ArrayUtil.isNotEmpty(t.value())) return t.value()[0];
                        if (ArrayUtil.isNotEmpty(t.path())) return t.path()[0];
                        return null;
                    });
                    break;
                case POST:
                    requestPath = getMethodRequestMapping(joinPoint, PostMapping.class).map(t -> {
                        if (ArrayUtil.isNotEmpty(t.value())) return t.value()[0];
                        if (ArrayUtil.isNotEmpty(t.path())) return t.path()[0];
                        return null;
                    });
                    break;
                case PUT:
                    requestPath = getMethodRequestMapping(joinPoint, PutMapping.class).map(t -> {
                        if (ArrayUtil.isNotEmpty(t.value())) return t.value()[0];
                        if (ArrayUtil.isNotEmpty(t.path())) return t.path()[0];
                        return null;
                    });
                    break;
                case DELETE:
                    requestPath = getMethodRequestMapping(joinPoint, DeleteMapping.class).map(t -> {
                        if (ArrayUtil.isNotEmpty(t.value())) return t.value()[0];
                        if (ArrayUtil.isNotEmpty(t.path())) return t.path()[0];
                        return null;
                    });
                    break;
                default:
                    requestPath = Optional.empty();
                    break;
            }
            if (classOfRequestPath.isPresent() && requestPath.isPresent()) {
                // 类上和方法上都有，拼装一个整体请求路径
                return getFullPath(classOfRequestPath, requestPath);
            } else if (requestPath.isPresent()) {
                // 类上没有，按方法上返回
                return requestPath.get();
            }
        }
        return null;
    }

    /**
     * 获取全路径
     *
     * @param classOfRequestPath
     * @param methodOfRequestPath
     * @return
     */
    private String getFullPath(Optional<String> classOfRequestPath, Optional<String> methodOfRequestPath) {
        final String firstRequestPath = classOfRequestPath.get();
        final String secondRequestPath = methodOfRequestPath.get();
        if (firstRequestPath.endsWith(StrUtil.SLASH) && secondRequestPath.startsWith(StrUtil.SLASH)) {
            return firstRequestPath + StrUtil.removePrefix(secondRequestPath, StrUtil.SLASH);
        } else if (!firstRequestPath.endsWith(StrUtil.SLASH) && !secondRequestPath.startsWith(StrUtil.SLASH)) {
            return firstRequestPath + StrUtil.SLASH + secondRequestPath;
        } else {
            return firstRequestPath + secondRequestPath;
        }
    }

    /**
     * 获取{@link JapiLimit}
     *
     * @param joinPoint {@link JoinPoint}
     * @return {@link Optional<JapiLimit>}
     */
    private Optional<JapiLimit> getCurrentLimit(JoinPoint joinPoint) {
        return Optional
                .of(joinPoint.getSignature())
                .map(v -> (MethodSignature) v)
                .map(MethodSignature::getMethod)
                .filter(v -> v.isAnnotationPresent(JapiLimit.class))
                .map(v -> v.getAnnotation(JapiLimit.class));
    }

    private <T extends Annotation> Optional<T> getMethodRequestMapping(JoinPoint joinPoint, Class<T> clazz) {
        return Optional
                .of(joinPoint.getSignature())
                .map(v -> (MethodSignature) v)
                .map(MethodSignature::getMethod)
                .filter(v -> v.isAnnotationPresent(clazz))
                .map(v -> v.getAnnotation(clazz));
    }

    /**
     * 获取类上的{@link RequestMapping}
     *
     * @param joinPoint
     * @return
     */
    private Optional<RequestMapping> getClassRequestMapping(JoinPoint joinPoint) {
        return Optional
                .of(joinPoint.getTarget())
                .map(Object::getClass)
                .filter(v -> v.isAnnotationPresent(RequestMapping.class))
                .map(v -> v.getAnnotation(RequestMapping.class));
    }

    @Override
    public void setEmbeddedValueResolver(StringValueResolver resolver) {
        this.resolver = resolver;
    }
}
